package com.progenies.ecg.asm31.model.codeparts.lines;

import java.lang.reflect.Method;
import java.util.Arrays;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.progenies.ecg.asm31.util.BytecodeInstructionsUtils;
import com.progenies.ecg.asm31.util.GenerationUtils;
import com.progenies.ecg.asm31.util.VariableRegister.Variable;
import com.progenies.ecg.model.ClassObject;
import com.progenies.ecg.model.ParameterObject;

/**
 * Define an instruction to invoke a method. It supports:
 *  - local methods, from the same class.
 *  - external methods, from a external class
 *  - static methods
 *  - instance methods, on a local or instance variable
 *  
 *  It can store the result of the invocation, optionally, on a local or instance variable. 
 * 
 * @author ofc587a87
 *
 */
public class InvokeMethodLinePart<E> extends ConfigurableLineCodePart
{
	private String methodName;
	private String storeVariableName;
	
	private Class<E> ownerClass; //for static methods
	private String variableOwner; //for instance methods
	
	private ParameterObject<?> params[];
	private Class<?> returnType;
	
	/** Automatic conversion between type at storing value */
	private boolean automaticStoreConversion=true;
	
	/**
	 * Constructor of the class. The result will not be stored by default.
	 * @param methodName Name of the method to invoke
	 * @param ownerClass Class owner of the method. It can null, meaning that the method is local to the current class.
	 * @param params parameters of the method. A null or empty array means no parameters. 
	 */
	InvokeMethodLinePart(String methodName, Class<E> ownerClass, ParameterObject<?> params[], Class<?> returnType)
	{
		this(methodName, ownerClass, params, returnType, (String)null);
	}

	/**
	 * Constructor of the class. The result will be stored.
	 * @param methodName Name of the method to invoke
	 * @param ownerClass Class owner of the method. It can null, meaning that the method is local to the current class.
	 * @param params parameters of the method. A null or empty array means no parameters.
	 * @param storeVariableName Name of the variable that will store the result
	 */
	InvokeMethodLinePart(String methodName, Class<E> ownerClass, ParameterObject<?>[] params, Class<?> returnType, String storeVariableName) {
		this.methodName=methodName;
		this.ownerClass=ownerClass;
		this.params=params;
		this.returnType=returnType;
		this.storeVariableName=storeVariableName;
	}
	
	
	/**
	 * Constructor of the class. The result will not be stored by default.
	 * @param methodName Name of the method to invoke
	 * @param ownerClass Class owner of the method. It can null, meaning that the method is local to the current class.
	 * @param params parameters of the method. A null or empty array means no parameters. 
	 */
	InvokeMethodLinePart(String methodName, String variableOwner, ParameterObject<?> params[], Class<?> returnType)
	{
		this(methodName, variableOwner, params, returnType, (String)null);
	}

	/**
	 * Constructor of the class. The result will be stored.
	 * @param methodName Name of the method to invoke
	 * @param ownerClass Class owner of the method. It can null, meaning that the method is local to the current class.
	 * @param params parameters of the method. A null or empty array means no parameters.
	 * @param storeVariableName Name of the variable that will store the result
	 */
	InvokeMethodLinePart(String methodName, String variableOwner, ParameterObject<?>[] params, Class<?> returnType, String storeVariableName) {
		this.methodName=methodName;
		this.variableOwner=variableOwner;
		this.params=params;
		this.returnType=returnType;
		this.storeVariableName=storeVariableName;
	}

	

	@Override
	public void generateBytecode() {
		if(this.visitor==null )
			throw new IllegalStateException("LinePart has not been configurated, please use ClassGenerator");
		
		Variable var=null; 
		String internalName=null;
		//variable method or static one?
		if(this.variableOwner!=null)
		{
			//1st special case: super & init method
			if(variableOwner.equals("super") && methodName.equals("<init>"))
			{
				var=varRegister.getVariable("this");
				ClassObject clsObject=GenerationUtils.getClassObject(parent);
				internalName=Type.getInternalName(clsObject.getSuperClass());
			} //2nd special case: this as owner
			else if(variableOwner.equals("this"))
			{
				var=varRegister.getVariable(variableOwner);
				ClassObject clsObject=GenerationUtils.getClassObject(parent);
				internalName=GenerationUtils.translateClassName(clsObject.getName());
			}
			else //3rd case: variable owner
			{
				var=varRegister.getVariable(variableOwner);
				internalName=var.getType().getInternalName();
			}
		}
		else //static method
		{
			if(ownerClass!=null) //external method
				internalName=Type.getInternalName(ownerClass);
			else //method of the same class
			{
				internalName=GenerationUtils.translateClassName(GenerationUtils.getClassObject(parent).getName());
			}
		}
		
		
		//if a variable must be loaded, i push it into stack
		if(var!=null)
		{
			if(var.isLocal())
				this.visitor.visitVarInsn(var.getType().getOpcode(Opcodes.ILOAD), var.getIndex());
			else if(var.isStatic())
				this.visitor.visitFieldInsn(Opcodes.GETSTATIC, GenerationUtils.getFieldOwner(var.getName(), parent), var.getName(), var.getType().getDescriptor());
			else
			{
				BytecodeInstructionsUtils.insertLocalVariableIntoStack(visitor, varRegister, "this");
				this.visitor.visitFieldInsn(Opcodes.GETFIELD, GenerationUtils.getFieldOwner(var.getName(), parent), var.getName(), var.getType().getDescriptor());
			}
		}
		
		//i'll load into stack any parameter
		if(this.params!=null)
		{
			for(ParameterObject<?> param: this.params)
			{
				if(param.getVariableName()!=null)
					BytecodeInstructionsUtils.insertLocalVariableIntoStack(visitor, varRegister, param.getVariableName());
				else
					BytecodeInstructionsUtils.insertValueIntoStack(visitor, param.getValue());
			}
		}
		
		//get real parent and real parameters
		try
		{
			//Class
			Class<?> cls=Class.forName(Type.getObjectType(internalName).getClassName());
			
//			System.out.println("Looking for "+methodName+"...");
			//search Method
			Method m=null;
			for(Method tmp : cls.getMethods())
			{
				if(tmp.getName().equals(methodName))
				{
//					System.out.println(" Found, comparing return type....");
					if(((tmp.getParameterTypes()==null || tmp.getParameterTypes().length==0) && (params==null || params.length==0))
							|| tmp.getParameterTypes().length == params.length)
					{
						//is returnType assignable?
						if(tmp.getReturnType()!=returnType)
						{
							if(!tmp.getReturnType().isAssignableFrom(returnType))
							{
//								System.out.println(" Return type "+tmp.getReturnType().getName()+" not assignable from "+returnType.getName());
								continue;
							}
						}
						
						//checks assignable parameters
//						System.out.println(" Comparing parameters....");
						boolean allAssignable=true;
						int i=0;
						for(Class<?> paramType : tmp.getParameterTypes())
						{
							if(!paramType.isAssignableFrom(params[i].getClassType()))
							{
//								System.out.println(" "+paramType.getName()+" not assignable from "+params[i].getClassType().getName());
								allAssignable=false;
								break;
							}
							
							params[i]=ParameterObject.changeType(params[i], paramType);
							i++;
							
						}
						
						if(allAssignable)
						{
							m=tmp;
							internalName=Type.getInternalName(m.getDeclaringClass());
//							System.out.println("     Internal name for method "+methodName+": "+internalName);
							break;
						}
					}
				}
			}
		}catch(ClassNotFoundException ex)
		{
			//Maybe, the class is autogenerated, so i'll use the user settings
		}
		catch(RuntimeException ex)
		{
			//Maybe, the class is autogenerated, so i'll use the user settings
		}
		
		this.visitor.visitMethodInsn(GenerationUtils.getInvokeType(methodName, variableOwner, ownerClass, internalName), internalName, methodName, GenerationUtils.translateDescription(params, returnType));
		
		if(storeVariableName!=null)
		{
			//if the target variable is "__^_return_^__", it's a special mark that tells me that i must return the value directly
			if(storeVariableName.equals("__^_return_^__"))
			{
				visitor.visitInsn(Type.getType(returnType).getOpcode(Opcodes.IRETURN));
				//TODO: Check conversion needed
			}
			else
			{		 
				Variable storeVar=varRegister.getVariable(storeVariableName);
				
				int conversionOpcode=GenerationUtils.getCastNeeded(Type.getType(this.returnType), storeVar.getType());
				if(!automaticStoreConversion && conversionOpcode!=Opcodes.NOP)
				{
					throw new RuntimeException("Conversion needed but disabled");
				}
				else if(automaticStoreConversion && conversionOpcode!=Opcodes.NOP)
				{
					if(conversionOpcode==Opcodes.CHECKCAST)
						visitor.visitTypeInsn(conversionOpcode, storeVar.getType().getInternalName());
					else
						visitor.visitInsn(conversionOpcode);
				}
				
				if(storeVar.isLocal())
					visitor.visitVarInsn(storeVar.getType().getOpcode(Opcodes.ISTORE), storeVar.getIndex());
				else
				{
					if(!storeVar.isStatic())
					{
						//to insert value into a isntance field, i need that the stack has "this" and the new value, in that order
						//At this point i have only the new value, so i must "play" with the stack
						BytecodeInstructionsUtils.insertLocalVariableIntoStack(visitor, varRegister, "this");
						if(storeVar.getType().getSize()==1) //type of storeVar (as its converted even if it's different
							visitor.visitInsn(Opcodes.DUP_X1); //duplicate the last value ('this', size 1) and insert it one position back
						else
							visitor.visitInsn(Opcodes.DUP_X2); //duplicate the last value ('this', size 1) and insert it TWO positions back

						visitor.visitInsn(Opcodes.POP); //delete the extra reference
					}
					
					visitor.visitFieldInsn(storeVar.isStatic()?Opcodes.PUTSTATIC:Opcodes.PUTFIELD, GenerationUtils.getFieldOwner(storeVar.getName(), parent), storeVar.getName(), storeVar.getType().getDescriptor());
				}
			}
		}
		
	}
	

	
	

	@Override
	public boolean isReturnCodePart() {
		return storeVariableName==null?false:storeVariableName.equals("__^_return_^__");
	}

	
	
	
	
	public String getMethodName() {
		return methodName;
	}

	public void setMethodName(String methodName) {
		this.methodName = methodName;
	}

	public String getStoreVariableName() {
		return storeVariableName;
	}

	public void setStoreVariableName(String storeVariableName) {
		this.storeVariableName = storeVariableName;
	}

	public Class<E> getOwnerClass() {
		return ownerClass;
	}

	public void setOwnerClass(Class<E> ownerClass) {
		this.ownerClass = ownerClass;
	}

	public ParameterObject<?>[] getParams() {
		return Arrays.copyOf(params, params.length);
	}

	public void setParams(ParameterObject<?> params[]) {
		this.params = Arrays.copyOf(params, params.length);
	}

	public String getVariableOwner() {
		return variableOwner;
	}

	public void setVariableOwner(String variableOwner) {
		this.variableOwner = variableOwner;
	}

	public boolean isAutomaticStoreConversion() {
		return automaticStoreConversion;
	}

	public void setAutomaticStoreConversion(boolean automaticStoreConversion) {
		this.automaticStoreConversion = automaticStoreConversion;
	}


}
